﻿using System;
using System.IO;
using FLib;
using LibraryGuard.Interfaces;
using LibraryGuard.Interfaces.Configuration;
using LibraryGuard.Interfaces.Utilities;

namespace LibraryGuard.Guards
{
	/// <summary>
	/// Path format guard.
	/// </summary>
	public class PathGuard
		: IGuard
	{
		#region Logger
		private static NLog.Logger Logger = NLog.LogManager.GetLogger("PathGuard");
		#endregion

		#region Private fields
		private readonly INameFormatter Formatter = null;
		private readonly IConfiguration BaseConfig = null;
		private readonly Configuration.PathGuard Config = null;

		private readonly object Padlock = new object();
		private readonly Internals.DefaultDataProvider DataProvider = new Internals.DefaultDataProvider();
		#endregion

		#region IGuard Members
		/// <inheritdoc />
		public bool OnlySpecificExtensions
		{
			get { return true; }
		}

		/// <inheritdoc />
		public IActionList ActionList { get; set; }

		/// <inheritdoc />
		public void Run()
		{
			this.Formatter.Compile();
			this.BaseConfig.Directories.ForEach(d => this.ActionList.Add(new GuardDirectory(d, this)));
		}

		/// <inheritdoc />
		public void FileSystemEvent(FileSystemEventArgs args, string baseDirectory)
		{
			if (args.ChangeType == WatcherChangeTypes.Changed || args.ChangeType == WatcherChangeTypes.Created || args.ChangeType == WatcherChangeTypes.Renamed)
			{
				Logger.Trace("File '{0}' created/changed, guard", args.FullPath);
				this.ActionList.Remove<MoveFile>(args.FullPath, Comparers.FSIString);
				this.ActionList.Add(new CheckFile(new FileInfo(args.FullPath), baseDirectory, this), Comparers.FSI);
			}
		}
		#endregion

		#region Constructor
		public PathGuard(IConfiguration cfg, IGuardConfiguration guard)
		{
			Validate.Debug(() => cfg, v => v.NotNull());
			Validate.Debug(() => guard, v => v.NotNull());

			this.BaseConfig = cfg;
			this.Config = (Configuration.PathGuard)guard;

			this.Formatter = new Utilities.NameFormatter(this.Config.Format);

			Logger.Info("Path guard enabled");
		}
		#endregion

		#region Actions
		/// <summary>
		/// Guards directory - lists files and subdirectories.
		/// </summary>
		private sealed class GuardDirectory
			: IActionInfo
		{
			#region Private fields
			private readonly DirectoryInfo Directory = null;
			private readonly PathGuard Parent = null;
			#endregion

			#region IActionInfo Members
			/// <inheritdoc />
			public object ActionId
			{
				get { return this.Directory; }
			}

			/// <inheritdoc />
			public bool IsEdgeAction
			{
				get { return false; }
			}

			/// <inheritdoc />
			public bool Execute(IActionList actionList)
			{
				this.Directory.Refresh();
				if (!this.Directory.Exists)
				{
					Logger.Debug("Directory '{0}' doesn't exist, ignoring", this.Directory.FullName);
					return true;
				}

				Logger.Trace("Searching in directory '{0}'", this.Directory.FullName);
				foreach (var ext in this.Parent.BaseConfig.Extensions)
				{
					FileInfo[] files = null;
					try
					{
						files = this.Directory.GetFiles("*." + ext, SearchOption.AllDirectories);
					}
					catch (Exception ex)
					{
						Logger.WarnException("Cannot guard files in folder '{0}' when searching for extension {1}, skipping".FormatWith(this.Directory.FullName, ext), ex);
						continue;
					}
					foreach (var file in files)
						actionList.Add(new CheckFile(file, this.Directory.FullName, this.Parent), Comparers.FSI);
				}
				return true;
			}
			#endregion

			#region Constructor
			public GuardDirectory(DirectoryInfo di, PathGuard parent)
			{
				this.Directory = di;
				this.Parent = parent;
			}
			#endregion
		}

		/// <summary>
		/// Checks file.
		/// </summary>
		private sealed class CheckFile
			: IActionInfo
		{
			#region Private fields
			private readonly FileInfo File = null;
			private readonly string BasePath = null;
			private readonly PathGuard Parent = null;
			#endregion

			#region IActionInfo Members
			/// <inheritdoc />
			public object ActionId
			{
				get { return this.File; }
			}

			/// <inheritdoc />
			public bool IsEdgeAction
			{
				get { return false; }
			}

			/// <inheritdoc />
			public bool Execute(IActionList actionList)
			{
				this.File.Refresh();
				if (!this.File.Exists)
				{
					Logger.Debug("File '{0}' doesn't exist, ignoring", this.File.FullName);
					return true;
				}

				Logger.Trace("Checking file {0}", this.File.FullName);
				string correct = string.Empty;
				try
				{
					lock (this.Parent.Padlock)
					{
						using (TagLib.File f = TagLib.File.Create(this.File.FullName, TagLib.ReadStyle.None))
						{
							this.Parent.DataProvider.CurrentFile = f;
							correct = this.Parent.Formatter.Format(this.Parent.DataProvider);
						}
					}
				}
				catch (Exception ex)
				{
					Logger.WarnException("Cannot guard file '{0}', probably file is corrupted, ignoring".FormatWith(this.File.FullName), ex);
					return true;
				}

				string pathToConsider = this.File.FullName.Remove(0, this.BasePath.Length);
				string fullCorrectPath = Path.Combine(this.BasePath, correct);
				if (correct != pathToConsider)
				{
					actionList.Replace(new MoveFile(this.File, fullCorrectPath), Comparers.FSI);
				}
				return true;
			}
			#endregion

			#region Constructor
			public CheckFile(FileInfo fi, string basePath, PathGuard parent)
			{
				this.File = fi;
				this.BasePath = basePath;
				this.Parent = parent;
			}
			#endregion
		}

		/// <summary>
		/// Edge action. Moves file.
		/// </summary>
		private sealed class MoveFile
			: IActionInfo
		{
			#region Private fields
			private readonly FileInfo File = null;
			private readonly string NewPath = string.Empty;
			#endregion

			#region IActionInfo Members
			/// <inheritdoc />
			public object ActionId
			{
				get { return this.File; }
			}

			/// <inheritdoc />
			public bool IsEdgeAction
			{
				get { return true; }
			}

			/// <inheritdoc />
			public bool Execute(IActionList actionList)
			{
				this.File.Refresh();
				if (!this.File.Exists)
				{
					Logger.Debug("File '{0}' doesn't exist, ignoring", this.File.FullName);
					return true;
				}

				try
				{
					CreatePath(this.NewPath);
					this.File.MoveTo(this.NewPath);
					Logger.Info("File succesfully moved to '{1}'", this.File.FullName, this.NewPath);
				}
				catch (Exception ex)
				{
					Logger.WarnException("Cannot move file '{0}' to '{1}'".FormatWith(this.File.FullName, NewPath), ex);
					return false;
				}
				return true;
			}
			#endregion

			#region Constructor
			public MoveFile(FileInfo fi, string newPath)
			{
				this.File = fi;
				this.NewPath = newPath;
			}
			#endregion

			#region ToString
			public override string ToString()
			{
				return "Move file '{0}' to '{1}'".FormatWith(this.File.FullName, this.NewPath);
			}
			#endregion
		}
		#endregion

		#region Auxiliary methods
		private static void CreatePath(string path)
		{
			var dirs = Path.GetDirectoryName(path).Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries);
			string p = dirs[0] + '\\';
			for (int i = 1; i < dirs.Length; i++)
			{
				p += dirs[i] + '\\';
				if (!Directory.Exists(p))
					Directory.CreateDirectory(p);
			}
		}
		#endregion
	}
}
